/**
 * 
 */
package net.phoenix.repository.query.sql;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import javax.jcr.Workspace;
import javax.jcr.query.InvalidQueryException;
import javax.jcr.query.QueryResult;
import javax.jcr.query.Row;
import javax.jcr.query.RowIterator;

import net.phoenix.repository.RepositoryRuntimeException;
import net.phoenix.repository.SQLParameter;
import net.phoenix.repository.dao.DatabaseException;
import net.phoenix.repository.dao.ValueTableSearchDao;
import net.phoenix.repository.query.GeneralQuery;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author lixf
 * 
 */
public class SQLQuery extends GeneralQuery {
	private static Log log = LogFactory.getLog(SQLQuery.class);
	private Map<Integer, Value> parameters;
	private SQLParameter[] sqlParameters;

	public SQLQuery() {
	}

	public void init(Workspace workspace, String statement)
			throws InvalidQueryException {
		super.init(workspace, statement);
		this.parameters = new HashMap<Integer, Value>();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.jcr.query.Query#getLanguage()
	 */
	@SuppressWarnings("deprecation")
	@Override
	public String getLanguage() {
		return SQL;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.jcr.query.Query#bindValue(java.lang.String, javax.jcr.Value)
	 */
	@Override
	public void bindValue(String varName, Value value)
			throws IllegalArgumentException, RepositoryException {
		this.parameters.put(new Integer(varName), value);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.jcr.query.Query#getBindVariableNames()
	 */
	@Override
	public String[] getBindVariableNames() throws RepositoryException {
		String[] result = new String[this.parameters.size()];
		for (int i = 0; i < result.length; i++)
			result[i] = "" + i;
		return result;
	}

	private void prepareParameters() throws ValueFormatException,
			IllegalStateException, RepositoryException {
		sqlParameters = new SQLParameter[parameters.size()];
		for (int pIndex : parameters.keySet()) {
			Value value = parameters.get(pIndex);
			SQLParameter param = new SQLParameter();
			switch (value.getType()) {
			case PropertyType.BINARY:
				param.setValue(value.getString());
				break;
			case PropertyType.BOOLEAN:
				param.setValue(value.getBoolean());
				break;
			case PropertyType.DATE:
				param.setValue(new Timestamp(value.getDate().getTimeInMillis()));
				break;
			case PropertyType.DECIMAL:
				param.setValue(value.getString());
				break;
			case PropertyType.DOUBLE:
				param.setValue(value.getString());
				break;
			case PropertyType.LONG:
				param.setValue(value.getString());
				break;
			case PropertyType.NAME:
				param.setValue(value.getString());
				break;
			case PropertyType.PATH:
				param.setValue(value.getString());
				break;
			case PropertyType.REFERENCE:
				param.setValue(value.getString());
				break;
			case PropertyType.STRING:
				param.setValue(value.getString());
				break;
			default:
				param.setValue(value.getString());
				break;
			}
			sqlParameters[pIndex - 1] = param;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.jcr.query.Query#execute()
	 */
	@Override
	public QueryResult execute() throws InvalidQueryException,
			RepositoryException {
		this.prepareParameters();

		return new SQLQueryResult();
	}

	/**
	 * @author lixf
	 * 
	 */
	public class SQLQueryResult implements QueryResult {
		private ResultSet result;
		private String[] columns;
		private int size;

		public SQLQueryResult() throws RepositoryException {
			this.result = null;
			log.debug("Execute query : " + statement);
			this.buildResultSet(offset);
			this.buildColumns();
			this.buildSize();
		}

		private void buildResultSet(final long from) throws RepositoryException {
			ValueTableSearchDao dao = session.getDao(ValueTableSearchDao.class);
			try {
				result = dao.search(statement, sqlParameters, from, limit);
			} catch (SQLException e) {
				throw new RepositoryException(e);
			}
		}

		private void buildColumns() {
			ResultSetMetaData meta;
			try {
				meta = result.getMetaData();
				this.columns = new String[meta.getColumnCount()];
				for (int i = 0; i < meta.getColumnCount(); i++)
					this.columns[i] = meta.getColumnLabel(i + 1);
			} catch (SQLException e) {
				throw new DatabaseException(e);
			}
		}

		private void buildSize() {
			try {
				result.last();
				size = result.getRow();
				result.beforeFirst();
			} catch (SQLException e) {
				throw new DatabaseException(e);
			}

		}

		@Override
		public String[] getColumnNames() throws RepositoryException {
			return this.columns;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.jcr.query.QueryResult#getRows()
		 */
		@Override
		public RowIterator getRows() throws RepositoryException {
			try {
				this.result.beforeFirst();
			} catch (SQLException e) {
				throw new RepositoryException(e);
			}
			return new CachedRowsetRowIterator();
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see javax.jcr.query.QueryResult#getNodes()
		 */
		@Override
		public NodeIterator getNodes() throws RepositoryException {
			try {
				this.result.beforeFirst();
			} catch (SQLException e) {
				throw new RepositoryException(e);
			}
			return new CachedRowsetNodeIterator();
		}

		@Override
		public String[] getSelectorNames() throws RepositoryException {
			return this.columns;
		}

		abstract class CachedRowsetIterator<T> {

			private long position;

			public CachedRowsetIterator() throws RepositoryException {
				this.position = 0;

			}

			/**
			 * Returns the current position within the iterator.
			 * 
			 * @return a long
			 */
			public long getPosition() {
				return this.position;
			}

			/**
			 * Returns the number of elements in the iterator.
			 * 
			 * @return a long
			 */
			public long getSize() {
				return size;
			}

			/**
			 * Returns <tt>true</tt> if the iteration has more elements.
			 * 
			 * @return <tt>true</tt> if the iterator has more elements.
			 */
			public boolean hasNext() {
				return position < size;
			}

			/**
			 * Returns the next element in the iteration.
			 * 
			 * @return the next element in the iteration.
			 */
			public T next() {
				if (!hasNext())
					throw new NoSuchElementException();
				try {
					if (result.next()) {
						this.position++;
						return resultToItem(result);
					}
				} catch (SQLException e) {
					throw new DatabaseException(e);
				}
				throw new NoSuchElementException();
			}

			abstract protected T resultToItem(ResultSet rs) throws SQLException;

			/**
			 * Skip a number of elements in the iterator.
			 * 
			 * @param skipNum
			 *            the non-negative number of elements to skip
			 */
			public void skip(long skipNum) {
				this.position += skipNum;
				if (!this.hasNext())
					throw new NoSuchElementException();
				try {
					result.absolute((int) position + 1);
				} catch (SQLException e) {
					throw new DatabaseException(e);

				}
			}

			/**
			 * Removes from the underlying collection the last element returned
			 * by the iterator (optional operation).
			 * 
			 * @todo Implement this java.util.Iterator method
			 */
			public void remove() {
				throw new UnsupportedOperationException();
			}
		}

		class CachedRowsetRowIterator extends CachedRowsetIterator<Row>
				implements RowIterator {
			public CachedRowsetRowIterator() throws RepositoryException {
			}

			@Override
			public Row nextRow() {
				return next();
			}

			@Override
			protected Row resultToItem(ResultSet rs) {
				return new ResultSetRow(workspace.getSession(), result);
			}
		}

		class CachedRowsetNodeIterator extends CachedRowsetIterator<Node>
				implements NodeIterator {

			public CachedRowsetNodeIterator() throws RepositoryException {
			}

			/**
			 * nextNode
			 * 
			 * @return Node
			 * @todo Implement this net.phoenix.repository.NodeIterator method
			 */
			@Override
			public Node nextNode() {
				return this.next();
			}

			@Override
			protected Node resultToItem(ResultSet rs) throws SQLException {
				String pkid = rs.getString("pkid");
				try {
					return workspace.getSession().getNodeByIdentifier(pkid);
				} catch (RepositoryRuntimeException e) {
					throw e;
				} catch (RepositoryException e) {
					throw new RepositoryRuntimeException(e);
				}
			}

		}
	}
}
